/*
 * Copyright (c) 2007 innoSysTec (R) GmbH, Germany. All rights reserved.
 * Original author: Edmund Wagner
 * Creation date: 31.05.2007
 *
 * Source: $HeadURL$
 * Last changed: $LastChangedDate$
 * 
 * the unrar licence applies to all junrar source and binary distributions 
 * you are not allowed to use this source to re-create the RAR compression algorithm
 * 
 * Here some html entities which can be used for escaping javadoc tags:
 * "&":  "&#038;" or "&amp;"
 * "<":  "&#060;" or "&lt;"
 * ">":  "&#062;" or "&gt;"
 * "@":  "&#064;" 
 */
package com.github.junrar.unpack.ppm;

import java.io.IOException;
import java.util.Arrays;

import com.github.junrar.exception.RarException;
import com.github.junrar.unpack.Unpack;

/**
 * DOCUMENT ME
 * 
 * @author $LastChangedBy$
 * @version $LastChangedRevision$
 */
public class ModelPPM
 {
  public static final int MAX_O = 64; /* maximum allowed model order */

  public static final int INT_BITS = 7;

  public static final int PERIOD_BITS = 7;

  public static final int TOT_BITS = INT_BITS + PERIOD_BITS;

  public static final int INTERVAL = 1 << INT_BITS;

  public static final int BIN_SCALE = 1 << TOT_BITS;

  public static final int MAX_FREQ = 124;

  private SEE2Context[][] SEE2Cont = new SEE2Context[25][16];

  private SEE2Context dummySEE2Cont;

  private PPMContext minContext, medContext, maxContext;

  private State foundState; // found next state transition

  private int numMasked, initEsc, orderFall, maxOrder, runLength, initRL;

  private int[] charMask = new int[256];

  private int[] NS2Indx = new int[256];

  private int[] NS2BSIndx = new int[256];

  private int[] HB2Flag = new int[256];

  // byte EscCount, PrevSuccess, HiBitsFlag;
  private int escCount, prevSuccess, hiBitsFlag;

  private int[][] binSumm = new int[128][64]; // binary SEE-contexts

  private RangeCoder coder = new RangeCoder();

  private SubAllocator subAlloc = new SubAllocator();

  private static int InitBinEsc[] = {0x3CDD, 0x1F3F, 0x59BF, 0x48F3, 0x64A1, 0x5ABC, 0x6632, 0x6051};

  // Temp fields
  private final State tempState1 = new State(null);
  private final State tempState2 = new State(null);
  private final State tempState3 = new State(null);
  private final State tempState4 = new State(null);
  private final StateRef tempStateRef1 = new StateRef();
  private final StateRef tempStateRef2 = new StateRef();
  private final PPMContext tempPPMContext1 = new PPMContext(null);
  private final PPMContext tempPPMContext2 = new PPMContext(null);
  private final PPMContext tempPPMContext3 = new PPMContext(null);
  private final PPMContext tempPPMContext4 = new PPMContext(null);
  private final int[] ps = new int[MAX_O];

  public ModelPPM()
   {
    minContext = null;
    maxContext = null;
    medContext = null;
   }

  public SubAllocator getSubAlloc()
   {
    return subAlloc;
   }

  private void restartModelRare()
   {
    Arrays.fill(charMask, 0);
    subAlloc.initSubAllocator();
    initRL = -(maxOrder < 12 ? maxOrder : 12) - 1;
    int addr = subAlloc.allocContext();
    minContext.setAddress(addr);
    maxContext.setAddress(addr);
    minContext.setSuffix(0);
    orderFall = maxOrder;
    minContext.setNumStats(256);
    minContext.getFreqData().setSummFreq(minContext.getNumStats() + 1);

    addr = subAlloc.allocUnits(256 / 2);
    foundState.setAddress(addr);
    minContext.getFreqData().setStats(addr);

    State state = new State(subAlloc.getHeap());
    addr = minContext.getFreqData().getStats();
    runLength = initRL;
    prevSuccess = 0;
    for (int i = 0; i < 256; i++)
     {
      state.setAddress(addr + i * State.size);
      state.setSymbol(i);
      state.setFreq(1);
      state.setSuccessor(0);
     }

    for (int i = 0; i < 128; i++)
     {
      for (int k = 0; k < 8; k++)
       {
        for (int m = 0; m < 64; m += 8)
         {
          binSumm[i][k + m] = BIN_SCALE - InitBinEsc[k] / (i + 2);
         }
       }
     }
    for (int i = 0; i < 25; i++)
     {
      for (int k = 0; k < 16; k++)
       {
        SEE2Cont[i][k].init(5 * i + 10);
       }
     }
   }

  private void startModelRare(int MaxOrder)
   {
    int i, k, m, Step;
    escCount = 1;
    this.maxOrder = MaxOrder;
    restartModelRare();
    // Bug Fixed
    NS2BSIndx[0] = 0;
    NS2BSIndx[1] = 2;
    for (int j = 0; j < 9; j++)
     {
      NS2BSIndx[2 + j] = 4;
     }
    for (int j = 0; j < 256 - 11; j++)
     {
      NS2BSIndx[11 + j] = 6;
     }
    for (i = 0; i < 3; i++)
     {
      NS2Indx[i] = i;
     }
    for (m = i, k = 1, Step = 1; i < 256; i++)
     {
      NS2Indx[i] = m;
      if ((--k) == 0)
       {
        k = ++Step;
        m++;
       }
     }
    for (int j = 0; j < 0x40; j++)
     {
      HB2Flag[j] = 0;
     }
    for (int j = 0; j < 0x100 - 0x40; j++)
     {
      HB2Flag[0x40 + j] = 0x08;
     }
    dummySEE2Cont.setShift(PERIOD_BITS);

   }

  private void clearMask()
   {
    escCount = 1;
    Arrays.fill(charMask, 0);
   }

  public boolean decodeInit(Unpack unpackRead, int escChar/* ref */) throws IOException, RarException
   {

    int MaxOrder = unpackRead.getChar() & 0xff;
    boolean reset = ((MaxOrder & 0x20) != 0);

    int MaxMB = 0;
    if (reset)
     {
      MaxMB = unpackRead.getChar();
     }
    else
     {
      if (subAlloc.GetAllocatedMemory() == 0)
       {
        return (false);
       }
     }
    if ((MaxOrder & 0x40) != 0)
     {
      escChar = unpackRead.getChar();
      unpackRead.setPpmEscChar(escChar);
     }
    coder.initDecoder(unpackRead);
    if (reset)
     {
      MaxOrder = (MaxOrder & 0x1f) + 1;
      if (MaxOrder > 16)
       {
        MaxOrder = 16 + (MaxOrder - 16) * 3;
       }
      if (MaxOrder == 1)
       {
        subAlloc.stopSubAllocator();
        return (false);
       }
      subAlloc.startSubAllocator(MaxMB + 1);
      minContext = new PPMContext(getHeap());
      medContext = new PPMContext(getHeap());
      maxContext = new PPMContext(getHeap());
      foundState = new State(getHeap());
      dummySEE2Cont = new SEE2Context();
      for (int i = 0; i < 25; i++)
       {
        for (int j = 0; j < 16; j++)
         {
          SEE2Cont[i][j] = new SEE2Context();
         }
       }
      startModelRare(MaxOrder);
     }
    return (minContext.getAddress() != 0);
   }

  public int decodeChar() throws IOException, RarException
   {
    // Debug
    // subAlloc.dumpHeap();

    if (minContext.getAddress() <= subAlloc.getPText() || minContext.getAddress() > subAlloc.getHeapEnd())
     {
      return (-1);
     }

    if (minContext.getNumStats() != 1)
     {
      if (minContext.getFreqData().getStats() <= subAlloc.getPText()
          || minContext.getFreqData().getStats() > subAlloc.getHeapEnd())
       {
        return (-1);
       }
      if (!minContext.decodeSymbol1(this))
       {
        return (-1);
       }
     }
    else
     {
      minContext.decodeBinSymbol(this);
     }
    coder.decode();
    while (foundState.getAddress() == 0)
     {
      coder.ariDecNormalize();
      do
       {
        orderFall++;
        minContext.setAddress(minContext.getSuffix());// =MinContext->Suffix;
        if (minContext.getAddress() <= subAlloc.getPText() || minContext.getAddress() > subAlloc.getHeapEnd())
         {
          return (-1);
         }
       }
      while (minContext.getNumStats() == numMasked);
      if (!minContext.decodeSymbol2(this))
       {
        return (-1);
       }
      coder.decode();
     }
    int Symbol = foundState.getSymbol();
    if ((orderFall == 0) && foundState.getSuccessor() > subAlloc.getPText())
     {
      // MinContext=MaxContext=FoundState->Successor;
      int addr = foundState.getSuccessor();
      minContext.setAddress(addr);
      maxContext.setAddress(addr);
     }
    else
     {
      updateModel();
      // this.foundState.setAddress(foundState.getAddress());//TODO just 4
      // debugging
      if (escCount == 0)
       {
        clearMask();
       }
     }
    coder.ariDecNormalize();// ARI_DEC_NORMALIZE(Coder.code,Coder.low,Coder.range,Coder.UnpackRead);
    return (Symbol);
   }

  public SEE2Context[][] getSEE2Cont()
   {
    return SEE2Cont;
   }

  public SEE2Context getDummySEE2Cont()
   {
    return dummySEE2Cont;
   }

  public int getInitRL()
   {
    return initRL;
   }

  public void setEscCount(int escCount)
   {
    this.escCount = escCount & 0xff;
   }

  public int getEscCount()
   {
    return escCount;
   }

  public void incEscCount(int dEscCount)
   {
    setEscCount(getEscCount() + dEscCount);
   }

  public int[] getCharMask()
   {
    return charMask;
   }

  public int getNumMasked()
   {
    return numMasked;
   }

  public void setNumMasked(int numMasked)
   {
    this.numMasked = numMasked;
   }

  public void setPrevSuccess(int prevSuccess)
   {
    this.prevSuccess = prevSuccess & 0xff;
   }

  public int getInitEsc()
   {
    return initEsc;
   }

  public void setInitEsc(int initEsc)
   {
    this.initEsc = initEsc;
   }

  public void setRunLength(int runLength)
   {
    this.runLength = runLength;
   }

  public int getRunLength()
   {
    return runLength;
   }

  public void incRunLength(int dRunLength)
   {
    setRunLength(getRunLength() + dRunLength);
   }

  public int getPrevSuccess()
   {
    return prevSuccess;
   }

  public int getHiBitsFlag()
   {
    return hiBitsFlag;
   }

  public void setHiBitsFlag(int hiBitsFlag)
   {
    this.hiBitsFlag = hiBitsFlag & 0xff;
   }

  public int[][] getBinSumm()
   {
    return binSumm;
   }

  public RangeCoder getCoder()
   {
    return coder;
   }

  public int[] getHB2Flag()
   {
    return HB2Flag;
   }

  public int[] getNS2BSIndx()
   {
    return NS2BSIndx;
   }

  public int[] getNS2Indx()
   {
    return NS2Indx;
   }

  public State getFoundState()
   {
    return foundState;
   }

  public byte[] getHeap()
   {
    return subAlloc.getHeap();
   }

  public int getOrderFall()
   {
    return orderFall;
   }

  private int /* ppmcontext ptr */createSuccessors(boolean Skip, State p1 /*
                                                                           * state
                                                                           * ptr
                                                                           */)
   {
    // State upState = tempState1.init(null);
    StateRef upState = tempStateRef2;
    State tempState = tempState1.init(getHeap());

    // PPM_CONTEXT* pc=MinContext, * UpBranch=FoundState->Successor;
    PPMContext pc = tempPPMContext1.init(getHeap());
    pc.setAddress(minContext.getAddress());
    PPMContext upBranch = tempPPMContext2.init(getHeap());
    upBranch.setAddress(foundState.getSuccessor());

    // STATE * p, * ps[MAX_O], ** pps=ps;
    State p = tempState2.init(getHeap());
    int pps = 0;

    boolean noLoop = false;

    if (!Skip)
     {
      ps[pps++] = foundState.getAddress();// *pps++ = FoundState;
      if (pc.getSuffix() == 0)
       {
        noLoop = true;
       }
     }
    if (!noLoop)
     {
      boolean loopEntry = false;
      if (p1.getAddress() != 0)
       {
        p.setAddress(p1.getAddress());
        pc.setAddress(pc.getSuffix());// =pc->Suffix;
        loopEntry = true;
       }
      do
       {
        if (!loopEntry)
         {
          pc.setAddress(pc.getSuffix());// pc=pc->Suffix;
          if (pc.getNumStats() != 1)
           {
            p.setAddress(pc.getFreqData().getStats());// p=pc->U.Stats
            if (p.getSymbol() != foundState.getSymbol())
             {
              do
               {
                p.incAddress();
               }
              while (p.getSymbol() != foundState.getSymbol());
             }
           }
          else
           {
            p.setAddress(pc.getOneState().getAddress());// p=&(pc->OneState);
           }
         }// LOOP_ENTRY:
        loopEntry = false;
        if (p.getSuccessor() != upBranch.getAddress())
         {
          pc.setAddress(p.getSuccessor());// =p->Successor;
          break;
         }
        ps[pps++] = p.getAddress();
       }
      while (pc.getSuffix() != 0);

     } // NO_LOOP:
    if (pps == 0)
     {
      return pc.getAddress();
     }
    upState.setSymbol(getHeap()[upBranch.getAddress()]);// UpState.Symbol=*(byte*)
    // UpBranch;
    // UpState.Successor=(PPM_CONTEXT*) (((byte*) UpBranch)+1);
    upState.setSuccessor(upBranch.getAddress() + 1); // TODO check if +1
                                                     // necessary
    if (pc.getNumStats() != 1)
     {
      if (pc.getAddress() <= subAlloc.getPText())
       {
        return (0);
       }
      p.setAddress(pc.getFreqData().getStats());
      if (p.getSymbol() != upState.getSymbol())
       {
        do
         {
          p.incAddress();
         }
        while (p.getSymbol() != upState.getSymbol());
       }
      int cf = p.getFreq() - 1;
      int s0 = pc.getFreqData().getSummFreq() - pc.getNumStats() - cf;
      // UpState.Freq=1+((2*cf <= s0)?(5*cf > s0):((2*cf+3*s0-1)/(2*s0)));
      upState.setFreq(1 + ((2 * cf <= s0) ? (5 * cf > s0 ? 1 : 0) : ((2 * cf + 3 * s0 - 1) / (2 * s0))));
     }
    else
     {
      upState.setFreq(pc.getOneState().getFreq());// UpState.Freq=pc->OneState.Freq;
     }
    do
     {
      // pc = pc->createChild(this,*--pps,UpState);
      tempState.setAddress(ps[--pps]);
      pc.setAddress(pc.createChild(this, tempState, upState));
      if (pc.getAddress() == 0)
       {
        return 0;
       }
     }
    while (pps != 0);
    return pc.getAddress();
   }

  private void updateModelRestart()
   {
    restartModelRare();
    escCount = 0;
   }

  private void updateModel()
   {
    // System.out.println("ModelPPM.updateModel()");
    // STATE fs = *FoundState, *p = NULL;
    StateRef fs = tempStateRef1;
    fs.setValues(foundState);
    State p = tempState3.init(getHeap());
    State tempState = tempState4.init(getHeap());

    PPMContext pc = tempPPMContext3.init(getHeap());
    PPMContext successor = tempPPMContext4.init(getHeap());

    int ns1, ns, cf, sf, s0;
    pc.setAddress(minContext.getSuffix());
    if (fs.getFreq() < MAX_FREQ / 4 && pc.getAddress() != 0)
     {
      if (pc.getNumStats() != 1)
       {
        p.setAddress(pc.getFreqData().getStats());
        if (p.getSymbol() != fs.getSymbol())
         {
          do
           {
            p.incAddress();
           }
          while (p.getSymbol() != fs.getSymbol());
          tempState.setAddress(p.getAddress() - State.size);
          if (p.getFreq() >= tempState.getFreq())
           {
            State.ppmdSwap(p, tempState);
            p.decAddress();
           }
         }
        if (p.getFreq() < MAX_FREQ - 9)
         {
          p.incFreq(2);
          pc.getFreqData().incSummFreq(2);
         }
       }
      else
       {
        p.setAddress(pc.getOneState().getAddress());
        if (p.getFreq() < 32)
         {
          p.incFreq(1);
         }
       }
     }
    if (orderFall == 0)
     {
      foundState.setSuccessor(createSuccessors(true, p));
      minContext.setAddress(foundState.getSuccessor());
      maxContext.setAddress(foundState.getSuccessor());
      if (minContext.getAddress() == 0)
       {
        updateModelRestart();
        return;
       }
      return;
     }
    subAlloc.getHeap()[subAlloc.getPText()] = (byte) fs.getSymbol();
    subAlloc.incPText();
    successor.setAddress(subAlloc.getPText());
    if (subAlloc.getPText() >= subAlloc.getFakeUnitsStart())
     {
      updateModelRestart();
      return;
     }
    // // Debug
    // subAlloc.dumpHeap();
    if (fs.getSuccessor() != 0)
     {
      if (fs.getSuccessor() <= subAlloc.getPText())
       {
        fs.setSuccessor(createSuccessors(false, p));
        if (fs.getSuccessor() == 0)
         {
          updateModelRestart();
          return;
         }
       }
      if (--orderFall == 0)
       {
        successor.setAddress(fs.getSuccessor());
        if (maxContext.getAddress() != minContext.getAddress())
         {
          subAlloc.decPText(1);
         }
       }
     }
    else
     {
      foundState.setSuccessor(successor.getAddress());
      fs.setSuccessor(minContext);
     }
    // // Debug
    // subAlloc.dumpHeap();
    ns = minContext.getNumStats();
    s0 = minContext.getFreqData().getSummFreq() - (ns) - (fs.getFreq() - 1);
    for (pc.setAddress(maxContext.getAddress()); pc.getAddress() != minContext.getAddress(); pc.setAddress(pc.getSuffix()))
     {
      if ((ns1 = pc.getNumStats()) != 1)
       {
        if ((ns1 & 1) == 0)
         {
          // System.out.println(ns1);
          pc.getFreqData().setStats(subAlloc.expandUnits(pc.getFreqData().getStats(), ns1 >>> 1));
          if (pc.getFreqData().getStats() == 0)
           {
            updateModelRestart();
            return;
           }
         }
        // bug fixed
        // int sum = ((2 * ns1 < ns) ? 1 : 0) +
        // 2 * ((4 * ((ns1 <= ns) ? 1 : 0)) & ((pc.getFreqData()
        // .getSummFreq() <= 8 * ns1) ? 1 : 0));
        int sum = ((2 * ns1 < ns) ? 1 : 0) + 2
                  * (((4 * ns1 <= ns) ? 1 : 0) & ((pc.getFreqData().getSummFreq() <= 8 * ns1) ? 1 : 0));
        pc.getFreqData().incSummFreq(sum);
       }
      else
       {
        p.setAddress(subAlloc.allocUnits(1));
        if (p.getAddress() == 0)
         {
          updateModelRestart();
          return;
         }
        p.setValues(pc.getOneState());
        pc.getFreqData().setStats(p);
        if (p.getFreq() < MAX_FREQ / 4 - 1)
         {
          p.incFreq(p.getFreq());
         }
        else
         {
          p.setFreq(MAX_FREQ - 4);
         }
        pc.getFreqData().setSummFreq((p.getFreq() + initEsc + (ns > 3 ? 1 : 0)));
       }
      cf = 2 * fs.getFreq() * (pc.getFreqData().getSummFreq() + 6);
      sf = s0 + pc.getFreqData().getSummFreq();
      if (cf < 6 * sf)
       {
        cf = 1 + (cf > sf ? 1 : 0) + (cf >= 4 * sf ? 1 : 0);
        pc.getFreqData().incSummFreq(3);
       }
      else
       {
        cf = 4 + (cf >= 9 * sf ? 1 : 0) + (cf >= 12 * sf ? 1 : 0) + (cf >= 15 * sf ? 1 : 0);
        pc.getFreqData().incSummFreq(cf);
       }
      p.setAddress(pc.getFreqData().getStats() + ns1 * State.size);
      p.setSuccessor(successor);
      p.setSymbol(fs.getSymbol());
      p.setFreq(cf);
      pc.setNumStats(++ns1);
     }

    int address = fs.getSuccessor();
    maxContext.setAddress(address);
    minContext.setAddress(address);
    // TODO-----debug
    // int pos = minContext.getFreqData().getStats();
    // State a = new State(getHeap());
    // a.setAddress(pos);
    // pos+=State.size;
    // a.setAddress(pos);
    // --dbg end
    return;
   }

  // Debug
  public String toString()
   {
    StringBuilder buffer = new StringBuilder();
    buffer.append("ModelPPM[");
    buffer.append("\n  numMasked=");
    buffer.append(numMasked);
    buffer.append("\n  initEsc=");
    buffer.append(initEsc);
    buffer.append("\n  orderFall=");
    buffer.append(orderFall);
    buffer.append("\n  maxOrder=");
    buffer.append(maxOrder);
    buffer.append("\n  runLength=");
    buffer.append(runLength);
    buffer.append("\n  initRL=");
    buffer.append(initRL);
    buffer.append("\n  escCount=");
    buffer.append(escCount);
    buffer.append("\n  prevSuccess=");
    buffer.append(prevSuccess);
    buffer.append("\n  foundState=");
    buffer.append(foundState);
    buffer.append("\n  coder=");
    buffer.append(coder);
    buffer.append("\n  subAlloc=");
    buffer.append(subAlloc);
    buffer.append("\n]");
    return buffer.toString();
   }

  // Debug
  // public void dumpHeap() {
  // subAlloc.dumpHeap();
  // }
 }
